/*
 * Copyright (c) 2014, 2024, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */
package jdk.graal.compiler.core.common.type;

import static jdk.vm.ci.meta.MetaUtil.getSimpleName;

import java.util.Arrays;
import java.util.Objects;
import java.util.function.Function;

import jdk.graal.compiler.core.common.calc.FloatConvert;
import jdk.graal.compiler.core.common.calc.FloatConvertCategory;
import jdk.graal.compiler.core.common.type.ArithmeticOpTable.BinaryOp.Add;
import jdk.graal.compiler.core.common.type.ArithmeticOpTable.BinaryOp.And;
import jdk.graal.compiler.core.common.type.ArithmeticOpTable.BinaryOp.Compress;
import jdk.graal.compiler.core.common.type.ArithmeticOpTable.BinaryOp.Div;
import jdk.graal.compiler.core.common.type.ArithmeticOpTable.BinaryOp.Expand;
import jdk.graal.compiler.core.common.type.ArithmeticOpTable.BinaryOp.Max;
import jdk.graal.compiler.core.common.type.ArithmeticOpTable.BinaryOp.Min;
import jdk.graal.compiler.core.common.type.ArithmeticOpTable.BinaryOp.Mul;
import jdk.graal.compiler.core.common.type.ArithmeticOpTable.BinaryOp.MulHigh;
import jdk.graal.compiler.core.common.type.ArithmeticOpTable.BinaryOp.Or;
import jdk.graal.compiler.core.common.type.ArithmeticOpTable.BinaryOp.Rem;
import jdk.graal.compiler.core.common.type.ArithmeticOpTable.BinaryOp.Sub;
import jdk.graal.compiler.core.common.type.ArithmeticOpTable.BinaryOp.UMax;
import jdk.graal.compiler.core.common.type.ArithmeticOpTable.BinaryOp.UMin;
import jdk.graal.compiler.core.common.type.ArithmeticOpTable.BinaryOp.UMulHigh;
import jdk.graal.compiler.core.common.type.ArithmeticOpTable.BinaryOp.Xor;
import jdk.graal.compiler.core.common.type.ArithmeticOpTable.IntegerConvertOp.Narrow;
import jdk.graal.compiler.core.common.type.ArithmeticOpTable.IntegerConvertOp.SignExtend;
import jdk.graal.compiler.core.common.type.ArithmeticOpTable.IntegerConvertOp.ZeroExtend;
import jdk.graal.compiler.core.common.type.ArithmeticOpTable.ShiftOp.Shl;
import jdk.graal.compiler.core.common.type.ArithmeticOpTable.ShiftOp.Shr;
import jdk.graal.compiler.core.common.type.ArithmeticOpTable.ShiftOp.UShr;
import jdk.graal.compiler.core.common.type.ArithmeticOpTable.TernaryOp.FMA;
import jdk.graal.compiler.core.common.type.ArithmeticOpTable.UnaryOp.Abs;
import jdk.graal.compiler.core.common.type.ArithmeticOpTable.UnaryOp.Neg;
import jdk.graal.compiler.core.common.type.ArithmeticOpTable.UnaryOp.Not;
import jdk.graal.compiler.core.common.type.ArithmeticOpTable.UnaryOp.Sqrt;
import jdk.graal.compiler.debug.GraalError;
import jdk.graal.compiler.util.CollectionsUtil;
import jdk.vm.ci.meta.Constant;
import jdk.vm.ci.meta.JavaKind;

/**
 * Information about arithmetic operations.
 */
public final class ArithmeticOpTable {

    private final UnaryOp<Neg> neg;
    private final BinaryOp<Add> add;
    private final BinaryOp<Sub> sub;

    private final BinaryOp<Mul> mul;
    private final BinaryOp<MulHigh> mulHigh;
    private final BinaryOp<UMulHigh> umulHigh;
    private final BinaryOp<Div> div;
    private final BinaryOp<Rem> rem;

    private final UnaryOp<Not> not;
    private final BinaryOp<And> and;
    private final BinaryOp<Or> or;
    private final BinaryOp<Xor> xor;

    private final ShiftOp<Shl> shl;
    private final ShiftOp<Shr> shr;
    private final ShiftOp<UShr> ushr;

    private final UnaryOp<Abs> abs;
    private final UnaryOp<Sqrt> sqrt;

    private final IntegerConvertOp<ZeroExtend> zeroExtend;
    private final IntegerConvertOp<SignExtend> signExtend;
    private final IntegerConvertOp<Narrow> narrow;

    // signed
    private final BinaryOp<Max> max;
    private final BinaryOp<Min> min;
    // unsigned
    private final BinaryOp<UMax> umax;
    private final BinaryOp<UMin> umin;

    private final TernaryOp<FMA> fma;

    private final ReinterpretOp reinterpret;

    private final BinaryOp<Compress> compress;
    private final BinaryOp<Expand> expand;

    private final FloatConvertOp[] floatConvert;
    private final int hash;

    public static ArithmeticOpTable forStamp(Stamp s) {
        if (s instanceof ArithmeticStamp) {
            return ((ArithmeticStamp) s).getOps();
        } else {
            return EMPTY;
        }
    }

    public BinaryOp<?>[] getBinaryOps() {
        return new BinaryOp<?>[]{add, sub, mul, mulHigh, umulHigh, div, rem, and, or, xor, max, min, umax, umin};
    }

    public UnaryOp<?>[] getUnaryOps() {
        return new UnaryOp<?>[]{neg, not, abs, sqrt};
    }

    public ShiftOp<?>[] getShiftOps() {
        return new ShiftOp<?>[]{shl, shr, ushr};
    }

    public IntegerConvertOp<?>[] getIntegerConvertOps() {
        return new IntegerConvertOp<?>[]{zeroExtend, signExtend, narrow};
    }

    public static final ArithmeticOpTable EMPTY = new ArithmeticOpTable(null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
                    null, null, null, null, null, null, null);

    public interface ArithmeticOpWrapper {

        <OP> UnaryOp<OP> wrapUnaryOp(UnaryOp<OP> op);

        <OP> BinaryOp<OP> wrapBinaryOp(BinaryOp<OP> op);

        <OP> TernaryOp<OP> wrapTernaryOp(TernaryOp<OP> op);

        <OP> ShiftOp<OP> wrapShiftOp(ShiftOp<OP> op);

        <OP> IntegerConvertOp<OP> wrapIntegerConvertOp(IntegerConvertOp<OP> op);

        ReinterpretOp wrapReinterpretOp(ReinterpretOp op);

        FloatConvertOp wrapFloatConvertOp(FloatConvertOp op);
    }

    private static <T> T wrapIfNonNull(Function<T, T> wrapper, T obj) {
        if (obj == null) {
            return null;
        } else {
            return wrapper.apply(obj);
        }
    }

    public static ArithmeticOpTable wrap(ArithmeticOpWrapper wrapper, ArithmeticOpTable inner) {
        UnaryOp<Neg> neg = wrapIfNonNull(wrapper::wrapUnaryOp, inner.getNeg());
        BinaryOp<Add> add = wrapIfNonNull(wrapper::wrapBinaryOp, inner.getAdd());
        BinaryOp<Sub> sub = wrapIfNonNull(wrapper::wrapBinaryOp, inner.getSub());

        BinaryOp<Mul> mul = wrapIfNonNull(wrapper::wrapBinaryOp, inner.getMul());
        BinaryOp<MulHigh> mulHigh = wrapIfNonNull(wrapper::wrapBinaryOp, inner.getMulHigh());
        BinaryOp<UMulHigh> umulHigh = wrapIfNonNull(wrapper::wrapBinaryOp, inner.getUMulHigh());
        BinaryOp<Div> div = wrapIfNonNull(wrapper::wrapBinaryOp, inner.getDiv());
        BinaryOp<Rem> rem = wrapIfNonNull(wrapper::wrapBinaryOp, inner.getRem());

        UnaryOp<Not> not = wrapIfNonNull(wrapper::wrapUnaryOp, inner.getNot());
        BinaryOp<And> and = wrapIfNonNull(wrapper::wrapBinaryOp, inner.getAnd());
        BinaryOp<Or> or = wrapIfNonNull(wrapper::wrapBinaryOp, inner.getOr());
        BinaryOp<Xor> xor = wrapIfNonNull(wrapper::wrapBinaryOp, inner.getXor());

        ShiftOp<Shl> shl = wrapIfNonNull(wrapper::wrapShiftOp, inner.getShl());
        ShiftOp<Shr> shr = wrapIfNonNull(wrapper::wrapShiftOp, inner.getShr());
        ShiftOp<UShr> ushr = wrapIfNonNull(wrapper::wrapShiftOp, inner.getUShr());

        UnaryOp<Abs> abs = wrapIfNonNull(wrapper::wrapUnaryOp, inner.getAbs());
        UnaryOp<Sqrt> sqrt = wrapIfNonNull(wrapper::wrapUnaryOp, inner.getSqrt());

        IntegerConvertOp<ZeroExtend> zeroExtend = wrapIfNonNull(wrapper::wrapIntegerConvertOp, inner.getZeroExtend());
        IntegerConvertOp<SignExtend> signExtend = wrapIfNonNull(wrapper::wrapIntegerConvertOp, inner.getSignExtend());
        IntegerConvertOp<Narrow> narrow = wrapIfNonNull(wrapper::wrapIntegerConvertOp, inner.getNarrow());

        BinaryOp<Max> max = wrapIfNonNull(wrapper::wrapBinaryOp, inner.getMax());
        BinaryOp<Min> min = wrapIfNonNull(wrapper::wrapBinaryOp, inner.getMin());
        BinaryOp<UMax> umax = wrapIfNonNull(wrapper::wrapBinaryOp, inner.getUMax());
        BinaryOp<UMin> umin = wrapIfNonNull(wrapper::wrapBinaryOp, inner.getUMin());

        TernaryOp<FMA> fma = wrapIfNonNull(wrapper::wrapTernaryOp, inner.getFMA());

        ReinterpretOp reinterpret = wrapIfNonNull(wrapper::wrapReinterpretOp, inner.getReinterpret());

        BinaryOp<Compress> compress = wrapIfNonNull(wrapper::wrapBinaryOp, inner.getCompress());
        BinaryOp<Expand> expand = wrapIfNonNull(wrapper::wrapBinaryOp, inner.getExpand());

        FloatConvertOp[] floatConvert = CollectionsUtil.filterAndMapToArray(inner.floatConvert, Objects::nonNull, wrapper::wrapFloatConvertOp, FloatConvertOp[]::new);
        return new ArithmeticOpTable(neg, add, sub, mul, mulHigh, umulHigh, div, rem, not, and, or, xor, shl, shr, ushr, abs, sqrt, zeroExtend, signExtend, narrow, max, min, umax, umin, fma,
                        reinterpret, compress, expand, floatConvert);
    }

    public ArithmeticOpTable(UnaryOp<Neg> neg, BinaryOp<Add> add, BinaryOp<Sub> sub, BinaryOp<Mul> mul, BinaryOp<MulHigh> mulHigh, BinaryOp<UMulHigh> umulHigh, BinaryOp<Div> div, BinaryOp<Rem> rem,
                    UnaryOp<Not> not, BinaryOp<And> and, BinaryOp<Or> or, BinaryOp<Xor> xor, ShiftOp<Shl> shl, ShiftOp<Shr> shr, ShiftOp<UShr> ushr, UnaryOp<Abs> abs, UnaryOp<Sqrt> sqrt,
                    IntegerConvertOp<ZeroExtend> zeroExtend, IntegerConvertOp<SignExtend> signExtend, IntegerConvertOp<Narrow> narrow, BinaryOp<Max> max, BinaryOp<Min> min, BinaryOp<UMax> umax,
                    BinaryOp<UMin> umin, TernaryOp<FMA> fma, ReinterpretOp reinterpret, BinaryOp<Compress> compress, BinaryOp<Expand> expand, FloatConvertOp... floatConvert) {
        this.neg = neg;
        this.add = add;
        this.sub = sub;
        this.mul = mul;
        this.mulHigh = mulHigh;
        this.umulHigh = umulHigh;
        this.div = div;
        this.rem = rem;
        this.not = not;
        this.and = and;
        this.or = or;
        this.xor = xor;
        this.shl = shl;
        this.shr = shr;
        this.ushr = ushr;
        this.abs = abs;
        this.sqrt = sqrt;
        this.zeroExtend = zeroExtend;
        this.signExtend = signExtend;
        this.narrow = narrow;
        this.max = max;
        this.min = min;
        this.umax = umax;
        this.umin = umin;
        this.fma = fma;
        this.reinterpret = reinterpret;
        this.compress = compress;
        this.expand = expand;
        this.floatConvert = new FloatConvertOp[FloatConvert.values().length];
        for (FloatConvertOp op : floatConvert) {
            this.floatConvert[op.getFloatConvert().ordinal()] = op;
        }

        this.hash = Objects.hash(neg, add, sub, mul, div, rem, not, and, or, xor, shl, shr, ushr, abs, sqrt, zeroExtend, signExtend, narrow, max, min, umax, umin, fma, reinterpret, compress, expand,
                        Arrays.hashCode(floatConvert));
    }

    @Override
    public int hashCode() {
        return hash;
    }

    /**
     * Describes the unary negation operation.
     */
    public UnaryOp<Neg> getNeg() {
        return neg;
    }

    /**
     * Describes the addition operation.
     */
    public BinaryOp<Add> getAdd() {
        return add;
    }

    /**
     * Describes the subtraction operation.
     */
    public BinaryOp<Sub> getSub() {
        return sub;
    }

    /**
     * Describes the multiplication operation.
     */
    public BinaryOp<Mul> getMul() {
        return mul;
    }

    /**
     * Describes a signed operation that multiples the upper 32-bits of two long values.
     */
    public BinaryOp<MulHigh> getMulHigh() {
        return mulHigh;
    }

    /**
     * Describes an unsigned operation that multiples the upper 32-bits of two long values.
     */
    public BinaryOp<UMulHigh> getUMulHigh() {
        return umulHigh;
    }

    /**
     * Describes the division operation.
     */
    public BinaryOp<Div> getDiv() {
        return div;
    }

    /**
     * Describes the remainder operation.
     */
    public BinaryOp<Rem> getRem() {
        return rem;
    }

    /**
     * Describes the bitwise not operation.
     */
    public UnaryOp<Not> getNot() {
        return not;
    }

    /**
     * Describes the bitwise and operation.
     */
    public BinaryOp<And> getAnd() {
        return and;
    }

    /**
     * Describes the bitwise or operation.
     */
    public BinaryOp<Or> getOr() {
        return or;
    }

    /**
     * Describes the bitwise xor operation.
     */
    public BinaryOp<Xor> getXor() {
        return xor;
    }

    /**
     * Describes the shift left operation.
     */
    public ShiftOp<Shl> getShl() {
        return shl;
    }

    /**
     * Describes the signed shift right operation.
     */
    public ShiftOp<Shr> getShr() {
        return shr;
    }

    /**
     * Describes the unsigned shift right operation.
     */
    public ShiftOp<UShr> getUShr() {
        return ushr;
    }

    /**
     * Describes the absolute value operation.
     */
    public UnaryOp<Abs> getAbs() {
        return abs;
    }

    /**
     * Describes the square root operation.
     */
    public UnaryOp<Sqrt> getSqrt() {
        return sqrt;
    }

    /**
     * Describes the zero extend conversion.
     */
    public IntegerConvertOp<ZeroExtend> getZeroExtend() {
        return zeroExtend;
    }

    /**
     * Describes the sign extend conversion.
     */
    public IntegerConvertOp<SignExtend> getSignExtend() {
        return signExtend;
    }

    /**
     * Describes the narrowing conversion.
     */
    public IntegerConvertOp<Narrow> getNarrow() {
        return narrow;
    }

    /**
     * Describes the Math.max operation.
     */
    public BinaryOp<Max> getMax() {
        return max;
    }

    /**
     * Describes the Math.min operation.
     */
    public BinaryOp<Min> getMin() {
        return min;
    }

    /**
     * Describes an unsigned integer max operation.
     */
    public BinaryOp<UMax> getUMax() {
        return umax;
    }

    /**
     * Describes an unsigned integer min operation.
     */
    public BinaryOp<UMin> getUMin() {
        return umin;
    }

    /**
     * Describes the fma operation (a * b + c).
     */
    public TernaryOp<FMA> getFMA() {
        return fma;
    }

    /**
     * Describes a reinterpret operation.
     */
    public ReinterpretOp getReinterpret() {
        return reinterpret;
    }

    /**
     * Describes an integer bit-compress operation.
     */
    public BinaryOp<Compress> getCompress() {
        return compress;
    }

    /**
     * Describes an integer bit-expand operation.
     */
    public BinaryOp<Expand> getExpand() {
        return expand;
    }

    /**
     * Describes integer/float/double conversions.
     */
    public FloatConvertOp getFloatConvert(FloatConvert op) {
        return floatConvert[op.ordinal()];
    }

    public static String toString(Op... ops) {
        return CollectionsUtil.mapAndJoin(ops, o -> o == null ? "null" : o.operator + "{" + getSimpleName(o.getClass(), false) + "}", ",");
    }

    private boolean opsEquals(ArithmeticOpTable that) {
        // @formatter:off
        return Objects.equals(neg, that.neg) &&
               Objects.equals(add, that.add) &&
               Objects.equals(sub, that.sub) &&
               Objects.equals(mul, that.mul) &&
               Objects.equals(mulHigh, that.mulHigh) &&
               Objects.equals(umulHigh, that.umulHigh) &&
               Objects.equals(div, that.div) &&
               Objects.equals(rem, that.rem) &&
               Objects.equals(not, that.not) &&
               Objects.equals(and, that.and) &&
               Objects.equals(or, that.or) &&
               Objects.equals(xor, that.xor) &&
               Objects.equals(shl, that.shl) &&
               Objects.equals(shr, that.shr) &&
               Objects.equals(ushr, that.ushr) &&
               Objects.equals(abs, that.abs) &&
               Objects.equals(sqrt, that.sqrt) &&
               Objects.equals(zeroExtend, that.zeroExtend) &&
               Objects.equals(signExtend, that.signExtend) &&
               Objects.equals(narrow, that.narrow) &&
               Objects.equals(max, that.max) &&
               Objects.equals(min, that.min) &&
               Objects.equals(umax, that.umax) &&
               Objects.equals(umin, that.umin) &&
               Objects.equals(fma, that.fma) &&
               Objects.equals(reinterpret, that.reinterpret) &&
               Objects.equals(compress, that.compress) &&
               Objects.equals(expand, that.expand);
        // @formatter:on
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null) {
            return false;
        }
        if (getClass() != obj.getClass()) {
            return false;
        }
        ArithmeticOpTable that = (ArithmeticOpTable) obj;
        if (opsEquals(that)) {
            if (Arrays.equals(this.floatConvert, that.floatConvert)) {
                return true;
            }
        }
        return false;
    }

    @Override
    public String toString() {
        return getClass().getSimpleName() + "[" +
                        toString(neg, add, sub, mul, mulHigh, umulHigh, div, rem, not, and, or, xor, shl, shr, ushr, abs, sqrt,
                                        zeroExtend, signExtend, narrow, max, min, umax, umin, fma, reinterpret, compress, expand) +
                        ",floatConvert[" + toString(floatConvert) + "]]";
    }

    public abstract static class Op {

        private final String operator;

        protected Op(String operator) {
            this.operator = operator;
        }

        @Override
        public String toString() {
            return operator;
        }

        @Override
        public int hashCode() {
            return operator.hashCode();
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj) {
                return true;
            }
            if (obj == null) {
                return false;
            }
            if (getClass() != obj.getClass()) {
                return false;
            }
            Op that = (Op) obj;
            if (operator.equals(that.operator)) {
                return true;
            }
            return true;
        }
    }

    /**
     * Describes a unary arithmetic operation.
     */
    public abstract static class UnaryOp<T> extends Op {

        public abstract static class Neg extends UnaryOp<Neg> {

            protected Neg() {
                super("-");
            }
        }

        public abstract static class Not extends UnaryOp<Not> {

            protected Not() {
                super("~");
            }
        }

        public abstract static class Abs extends UnaryOp<Abs> {

            protected Abs() {
                super("ABS");
            }
        }

        public abstract static class Sqrt extends UnaryOp<Sqrt> {

            protected Sqrt() {
                super("SQRT");
            }
        }

        protected UnaryOp(String operation) {
            super(operation);
        }

        /**
         * Apply the operation to a {@link Constant}.
         */
        public abstract Constant foldConstant(Constant value);

        /**
         * Apply the operation to a {@link Stamp}.
         */
        public final Stamp foldStamp(Stamp stamp) {
            Stamp result = foldStampImpl(stamp);
            GraalError.guarantee(!result.isEmpty() || stamp.isEmpty(), "empty stamps are not permitted when folding");

            return result;
        }

        protected abstract Stamp foldStampImpl(Stamp stamp);

        public UnaryOp<T> unwrap() {
            return this;
        }
    }

    /**
     * Describes a binary arithmetic operation.
     */
    public abstract static class BinaryOp<T> extends Op {

        public abstract static class Add extends BinaryOp<Add> {

            protected Add(boolean associative, boolean commutative) {
                super("+", associative, commutative);
            }
        }

        public abstract static class Sub extends BinaryOp<Sub> {

            protected Sub(boolean associative, boolean commutative) {
                super("-", associative, commutative);
            }
        }

        public abstract static class Mul extends BinaryOp<Mul> {

            protected Mul(boolean associative, boolean commutative) {
                super("*", associative, commutative);
            }
        }

        public abstract static class MulHigh extends BinaryOp<MulHigh> {

            protected MulHigh(boolean associative, boolean commutative) {
                super("*H", associative, commutative);
            }
        }

        public abstract static class UMulHigh extends BinaryOp<UMulHigh> {

            protected UMulHigh(boolean associative, boolean commutative) {
                super("|*H|", associative, commutative);
            }
        }

        public abstract static class Div extends BinaryOp<Div> {

            protected Div(boolean associative, boolean commutative) {
                super("/", associative, commutative);
            }
        }

        public abstract static class Rem extends BinaryOp<Rem> {

            protected Rem(boolean associative, boolean commutative) {
                super("%", associative, commutative);
            }
        }

        public abstract static class And extends BinaryOp<And> {

            protected And(boolean associative, boolean commutative) {
                super("&", associative, commutative);
            }
        }

        public abstract static class Or extends BinaryOp<Or> {

            protected Or(boolean associative, boolean commutative) {
                super("|", associative, commutative);
            }
        }

        public abstract static class Xor extends BinaryOp<Xor> {

            protected Xor(boolean associative, boolean commutative) {
                super("^", associative, commutative);
            }
        }

        public abstract static class Max extends BinaryOp<Max> {

            protected Max(boolean associative, boolean commutative) {
                super("MAX", associative, commutative);
            }
        }

        public abstract static class Min extends BinaryOp<Min> {

            protected Min(boolean associative, boolean commutative) {
                super("MIN", associative, commutative);
            }
        }

        public abstract static class UMax extends BinaryOp<UMax> {

            protected UMax(boolean associative, boolean commutative) {
                super("UMAX", associative, commutative);
            }
        }

        public abstract static class UMin extends BinaryOp<UMin> {

            protected UMin(boolean associative, boolean commutative) {
                super("UMIN", associative, commutative);
            }
        }

        public abstract static class Compress extends BinaryOp<Compress> {

            protected Compress(boolean associative, boolean commutative) {
                super("COMPRESS", associative, commutative);
            }
        }

        public abstract static class Expand extends BinaryOp<Expand> {

            protected Expand(boolean associative, boolean commutative) {
                super("EXPAND", associative, commutative);
            }
        }

        private final boolean associative;
        private final boolean commutative;

        protected BinaryOp(String operation, boolean associative, boolean commutative) {
            super(operation);
            this.associative = associative;
            this.commutative = commutative;
        }

        /**
         * Applies this operation to {@code a} and {@code b}.
         *
         * @return the result of applying this operation or {@code null} if applying it would raise
         *         an exception (e.g., {@link ArithmeticException} for dividing by 0)
         */
        public abstract Constant foldConstant(Constant a, Constant b);

        /**
         * Apply the operation to two {@linkplain Stamp Stamps}.
         */
        public final Stamp foldStamp(Stamp stamp1, Stamp stamp2) {
            Stamp result = foldStampImpl(stamp1, stamp2);
            GraalError.guarantee(!result.isEmpty() || (stamp1.isEmpty() || stamp2.isEmpty()), "empty stamps are not permitted when folding");
            return result;
        }

        /**
         * Internal extension point for subclasses. The inputs have already been checked for the
         * empty stamp and the return value must not be empty.
         */
        protected abstract Stamp foldStampImpl(Stamp a, Stamp b);

        /**
         * Checks whether this operation is associative. An operation is associative when
         * {@code (a . b) . c == a . (b . c)} for all a, b, c. Note that you still have to be
         * careful with inverses. For example the integer subtraction operation will report
         * {@code true} here, since you can still reassociate as long as the correct negations are
         * inserted.
         */
        public final boolean isAssociative() {
            return associative;
        }

        /**
         * Checks whether this operation is commutative. An operation is commutative when
         * {@code a . b == b . a} for all a, b.
         */
        public final boolean isCommutative() {
            return commutative;
        }

        /**
         * Check whether a {@link Constant} is a neutral element for this operation. A neutral
         * element is any element {@code n} where {@code a . n == a} for all a.
         *
         * @param n the {@link Constant} that should be tested
         * @return true iff for all {@code a}: {@code a . n == a}
         */
        public boolean isNeutral(Constant n) {
            return false;
        }

        /**
         * Check whether this operation has a zero {@code z == a . a} for each a. Examples of
         * operations having such an element are subtraction and exclusive-or. Note that this may be
         * different from the numbers tested by {@link #isNeutral}.
         *
         * @param stamp a {@link Stamp}
         * @return a unique {@code z} such that {@code z == a . a} for each {@code a} in
         *         {@code stamp} if it exists, otherwise {@code null}
         */
        public Constant getZero(Stamp stamp) {
            return null;
        }

        public BinaryOp<T> unwrap() {
            return this;
        }

        @Override
        public int hashCode() {
            final int prime = 31;
            int result = super.hashCode();
            result = prime * result + (associative ? 1231 : 1237);
            result = prime * result + (commutative ? 1231 : 1237);
            return result;
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj) {
                return true;
            }
            if (!super.equals(obj)) {
                return false;
            }
            if (getClass() != obj.getClass()) {
                return false;
            }
            BinaryOp<?> that = (BinaryOp<?>) obj;
            if (associative != that.associative) {
                return false;
            }
            if (commutative != that.commutative) {
                return false;
            }
            return true;
        }

        @Override
        public String toString() {
            if (associative) {
                if (commutative) {
                    return super.toString() + "[AC]";
                } else {
                    return super.toString() + "[A]";
                }
            } else if (commutative) {
                return super.toString() + "[C]";
            }
            return super.toString();
        }
    }

    public abstract static class TernaryOp<OP> extends Op {

        /**
         * This represents the operation a * b + c.
         */
        public abstract static class FMA extends TernaryOp<FMA> {

            protected FMA() {
                super("FMA");
            }
        }

        protected TernaryOp(String operation) {
            super(operation);
        }

        public abstract Constant foldConstant(Constant a, Constant b, Constant c);

        public abstract Stamp foldStamp(Stamp a, Stamp b, Stamp c);
    }

    /**
     * Describes a shift operation. The right argument of a shift operation always has kind
     * {@link JavaKind#Int}.
     */
    public abstract static class ShiftOp<OP> extends BinaryOp<OP> {

        public abstract static class Shl extends ShiftOp<Shl> {

            public Shl() {
                super("<<");
            }
        }

        public abstract static class Shr extends ShiftOp<Shr> {

            public Shr() {
                super(">>");
            }
        }

        public abstract static class UShr extends ShiftOp<UShr> {

            public UShr() {
                super(">>>");
            }
        }

        protected ShiftOp(String operation) {
            super(operation, false, false);
        }

        /**
         * Get the shift amount mask for a given result stamp.
         */
        public abstract int getShiftAmountMask(Stamp s);
    }

    public abstract static class FloatConvertOp extends UnaryOp<FloatConvertOp> {

        private final FloatConvert op;

        protected FloatConvertOp(FloatConvert op) {
            super(op.name());
            this.op = op;
        }

        public FloatConvert getFloatConvert() {
            return op;
        }

        @Override
        public FloatConvertOp unwrap() {
            return this;
        }

        /** Determine if this input is a floating-point stamp that can be NaN. */
        public boolean inputCanBeNaN(Stamp inputStamp) {
            return inputStamp instanceof FloatStamp floatStamp && floatStamp.canBeNaN();
        }

        /**
         * Determine if this is a floating-point to integer conversion whose result may be outside
         * the range of the target type.
         */
        @SuppressWarnings("unused")
        public boolean canOverflowInteger(Stamp inputStamp) {
            if (op.getCategory().equals(FloatConvertCategory.FloatingPointToInteger)) {
                throw GraalError.unimplementedOverride();
            }
            return false;
        }

        @Override
        public int hashCode() {
            final int prime = 31;
            return prime * super.hashCode() + op.hashCode();
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj) {
                return true;
            }
            if (!super.equals(obj)) {
                return false;
            }
            if (getClass() != obj.getClass()) {
                return false;
            }
            FloatConvertOp that = (FloatConvertOp) obj;
            if (op != that.op) {
                return false;
            }
            return true;
        }
    }

    public abstract static class ReinterpretOp extends Op {

        public ReinterpretOp() {
            super("Reinterpret");
        }

        public abstract Constant foldConstant(Stamp resultStamp, Constant c);

        public abstract Stamp foldStamp(Stamp resultStamp, Stamp input);
    }

    public abstract static class IntegerConvertOp<T> extends Op {

        public abstract static class ZeroExtend extends IntegerConvertOp<ZeroExtend> {

            protected ZeroExtend() {
                super("ZeroExtend");
            }
        }

        public abstract static class SignExtend extends IntegerConvertOp<SignExtend> {

            protected SignExtend() {
                super("SignExtend");
            }
        }

        public abstract static class Narrow extends IntegerConvertOp<Narrow> {

            protected Narrow() {
                super("Narrow");
            }

            @Override
            public Stamp invertStamp(int inputBits, int resultBits, Stamp outStamp) {
                return null;
            }
        }

        protected IntegerConvertOp(String op) {
            super(op);
        }

        public abstract Constant foldConstant(int inputBits, int resultBits, Constant value);

        public abstract Stamp foldStamp(int inputBits, int resultBits, Stamp stamp);

        public IntegerConvertOp<T> unwrap() {
            return this;
        }

        /**
         * Computes the stamp of the input for the given output stamp. This method returns
         * {@code null} if a stamp cannot be inverted for an operation (i.e., {@link Narrow}). When
         * inverting non-exact stamps, i.e. {@code 0xx0xxxx}, the inversion keeps all available
         * information. If the stamp to invert contains contradictions regarding the post condition
         * of the operation, an empty stamp is returned. An example for a contradiction would be a
         * {@code SignExtend} with both {@code 0} and {@code 1} in the extension.
         *
         * @return {@code null} - if stamp inversion is not supported </br>
         *         empty stamp - if the output stamp contains contradictions </br>
         *         inverted output stamp - otherwise </br>
         */
        public abstract Stamp invertStamp(int inputBits, int resultBits, Stamp outStamp);
    }
}
